using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using Microsoft.Xna.Framework.Content;
using XDL.Framework;
using XDL.Framework.Graphics;
using XDL.Framework.Collections;

namespace XDL.Framework.Particles
{
    public class LineSystem :
       DrawableGameComponent
    {
        // Name of the XML settings file describing this particle system.
        string m_settingsName;

        // Asset database
        ContentManager m_content;

        Effect m_effect;
        // Shortcuts for accessing frequently changed effect parameters.
        EffectParameter m_effectViewParameter;
        EffectParameter m_effectProjectionParameter;
        EffectParameter m_effectCameraPosition;

        DynamicVertexBuffer m_lineVertexBuffer;
        IndexBuffer m_lineIndexBuffer;

        // an array of line vertex element
        // note: all elements are allocated in quad (*same parameters*)
        LineVertex[] m_lines;
        //
        int m_activeLineCount = 0;

        LineSettings m_settings;
        public LineSettings Settings
        {
            get
            {
                return m_settings;
            }
        }

        static readonly Random m_random = new Random();

        /// <summary>
        /// External handler to update particle position (ex: collision management)
        /// </summary>
        /// <param name="gameTime">Upate time.</param>
        /// <param name="particle">Particle being updated.</param>
        /// <returns>false if the particle is to killed, true otherwise.</returns>
        public delegate bool LineUpdateHandler(
            GameTime gameTime,
            LineVertex oldParticle,
            ref LineVertex particle);

        /// <summary>
        /// External particle handler (optional)
        /// </summary>
        public LineUpdateHandler OnLineUpdate = null;

        public LineSystem(Game game, ContentManager content, string settingsName)
            :
            base(game)
        {
            m_settingsName = settingsName;
            m_content = content;
        }

        /// <summary>
        /// Clears all active lines.
        /// </summary>
        public void Clear()
        {
            m_activeLineCount = 0;
        }

        public override void Initialize()
        {
            m_settings = m_content.Load<LineSettings>(m_settingsName);
            InitializeEffect();

            // allocate 4 vertices per line
            m_lines = new LineVertex[m_settings.MaxLines * 4];

            // validate settings
            if (m_settings.DurationRandomness < 0 ||
                m_settings.DurationRandomness > 1)
                throw new InvalidOperationException("DurationRandomness must be within [0..1] range.");

            // create the buffer object
            m_lineVertexBuffer = new DynamicVertexBuffer(Game.GraphicsDevice, LineVertex.VertexDeclaration,
                                                   m_settings.MaxLines * 4, BufferUsage.WriteOnly);
            // create the index buffer
            int[] indices = { 0, 1, 2, 2, 0, 3 };
            int[] lineIndices = new int[m_settings.MaxLines * 3 * 2];
            for (int i = 0; i<m_settings.MaxLines * 3 * 2; i++)
            {
                lineIndices[i] = indices[i % 6] + (i / 6) * 4;
            }
            m_lineIndexBuffer = new IndexBuffer(
                Game.GraphicsDevice,
                typeof(int), lineIndices.Length,
                BufferUsage.WriteOnly);

            m_lineIndexBuffer.SetData<int>(lineIndices);
        }

        /// <summary>
        /// Helper for initializing the line effect.
        /// </summary>
        void InitializeEffect()
        {
            m_effect = m_settings.LineEffect;

            EffectParameterCollection parameters = m_effect.Parameters;

            // Look up shortcuts for parameters that change every frame.
            m_effectViewParameter = parameters["View"];
            m_effectProjectionParameter = parameters["Projection"];
            m_effectCameraPosition = parameters["CameraPosition"];

            // Choose the appropriate effect technique.
            m_effect.CurrentTechnique = m_effect.Techniques[m_settings.TechniqueName];
        }

        /// <summary>
        /// Updates line positions and velocity
        /// </summary>
        /// <param name="gameTime">Current time.</param>
        public override void Update(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
            int n = m_activeLineCount;
            m_activeLineCount = 0;
            for (int i = 0; i < n; i++)
            {
                m_lines[i].Lifetime -= dt;
                if (m_lines[i].Lifetime > 0)
                {
                    // as per Shawn Heargraves recommendations, don't use overloaded operators
                    m_lines[i].Position.X += dt * m_lines[i].Velocity.X;
                    m_lines[i].Position.Y += dt * m_lines[i].Velocity.Y;
                    m_lines[i].Position.Z += dt * m_lines[i].Velocity.Z;
                    // basic rebound behavior
                    if (m_settings.GroundLevel.HasValue && m_lines[i].Position.Z < m_settings.GroundLevel.Value)
                    {
                        m_lines[i].Position.Z = m_settings.GroundLevel.Value;
                        m_lines[i].Velocity.Z = -m_lines[i].Velocity.Z;
                    }
                    m_lines[i].Velocity.X += dt * m_settings.Gravity.X;
                    m_lines[i].Velocity.Y += dt * m_settings.Gravity.Y;
                    m_lines[i].Velocity.Z += dt * m_settings.Gravity.Z;
                    m_lines[i].Velocity.X += dt * m_settings.EndVelocity;
                    m_lines[i].Velocity.Y += dt * m_settings.EndVelocity;
                    m_lines[i].Velocity.Z += dt * m_settings.EndVelocity;

                    // re-activate the TracerBullet
                    if (m_activeLineCount != i)
                        m_lines[m_activeLineCount] = m_lines[i];

                    m_activeLineCount++;
                }
            }            
        }

        /// <summary>
        /// Add a new line
        /// </summary>
        /// <param name="start">Line tip position</param>
        /// <param name="end">Line end position</param>
        /// <param name="velocity">Emitter velocity</param>
        public void AddLine(Vector3 start, Vector3 end, Vector3 velocity)
        {
            // make sure we can add 4 tracer bullets (for a quad)
            if (m_activeLineCount < m_lines.Length - 4)
            {
                // Adjust the input velocity based on how much
                // this particle system wants to be affected by it.
                velocity *= m_settings.EmitterVelocitySensitivity;

                // Add in some random amount of horizontal velocity.
                // TODO: review - generate a random vector inside a cone (min/max)
                Matrix rotTransform = Matrix.CreateFromAxisAngle(
                    velocity,
                    MathHelper.TwoPi * (float)m_random.NextDouble());

                float lineVelocity = MathHelper.Lerp(m_settings.MinVelocity,
                                                           m_settings.MaxVelocity,
                                                           (float)m_random.NextDouble());

                velocity += (lineVelocity * rotTransform.Up);

                // Choose four random control values. These will be used by the vertex
                // shader to give each particle a different size, rotation, and color.
                Color randomValues = new Color((byte)m_random.Next(255),
                                               (byte)m_random.Next(255),
                                               (byte)m_random.Next(255),
                                               (byte)m_random.Next(255));

                LineVertex lv = new LineVertex();
                lv.Position = start;
                lv.Velocity = velocity;
                lv.Random = randomValues;
                // adjust the lifetime with the randomness factor
                lv.Lifetime = (float)(m_settings.Duration.TotalSeconds * (1.0 - m_random.NextDouble() * m_settings.DurationRandomness));

                m_lines[m_activeLineCount] = lv;
                lv.Texcoord.Y = 1;
                m_lines[m_activeLineCount + 3] = lv;
                // offset the tip of the line
                lv.Position = end;
                //
                lv.Texcoord.X = 1;
                lv.Texcoord.Y = 0;
                m_lines[m_activeLineCount + 1] = lv;
                lv.Texcoord.Y = 1;
                m_lines[m_activeLineCount + 2] = lv;

                //
                m_activeLineCount += 4;
            }
        }

        /// <summary>
        /// Sets the camera view and projection matrices
        /// that will be used to draw this particle system.
        /// </summary>
        public void SetCamera(Matrix view, Matrix projection)
        {
            m_effectViewParameter.SetValue(view);
            m_effectProjectionParameter.SetValue(projection);
            m_effectCameraPosition.SetValue(view.Translation);
        }

        public override void  Draw(GameTime gameTime)
        {
            if (m_activeLineCount == 0)
                return;

            /*
            if (technique != "TransparentTechnique")
                return;
            */

            // did we loose a segment start/end???
            System.Diagnostics.Debug.Assert((m_activeLineCount % 4) == 0);

            // helper
            GraphicsDevice gd = Game.GraphicsDevice;


            // update current GPU vertex format
            m_lineVertexBuffer.SetData(m_lines);
            gd.SetVertexBuffer(m_lineVertexBuffer);
            gd.Indices = m_lineIndexBuffer;

            // set misc render states
            gd.RasterizerState = RasterizerState.CullNone;
            gd.BlendState = m_settings.BlendState;
            gd.DepthStencilState = DepthStencilState.DepthRead;

            // render
            foreach (EffectPass pass in m_effect.CurrentTechnique.Passes)
            {
                pass.Apply();

                // 1 single call!!
                gd.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList,
                    0, 0,
                    m_activeLineCount * 4,
                    0,
                    m_activeLineCount * 2);
            }

            // restore state
            gd.RasterizerState = RasterizerState.CullCounterClockwise;
            gd.DepthStencilState = DepthStencilState.Default;
        }
    }
}
